This is a beginner-friendly tutorial series that teaches Solana smart contract development from the very basics.
You may notice that writing smart contract logic is relatively lightweight. The complexity lies mostly in different #[account]
macro usages and understanding parameters like whether accounts can be auto-created, how many bytes to reserve, etc. Because all account data must be loaded into Solana validator memory—which is costly—developers need to be precise with space usage. Solana’s account model also requires some understanding.
For standard tokens like USDT, Solana provides built-in libraries and command-line tools—no need to write contracts. These are called SPL Tokens. To create a token with 6 decimal places:
spl-token create-token --decimals 6
After executing, you’ll see an Address
, which is your token’s address, e.g., E75GMXAfJ91XuRboSpjwkDmta45Etgt3F3Gf5WLZvLbV
. You can look it up in the block explorer.
Next, create an Associated Token Account (ATA) for your wallet:
spl-token create-account E75GMXAfJ91XuRboSpjwkDmta45Etgt3F3Gf5WLZvLbV
This creates a record in the token contract’s internal map. Without it, your address can’t hold USDT. To view your ATA:
spl-token address --verbose --token E75GMXAfJ91XuRboSpjwkDmta45Etgt3F3Gf5WLZvLbV
Example output:
Wallet address: 75sFifxBt7zw1YrDfCdPjDCGDyKEqLWrBarPCLg6PHwb
Associated token address: E5XmcEJhhGUri8itThLGk8QfPzY1acFid8JmVyo5DWUo
Check token balance:
spl-token balance E75GMXAfJ91XuRboSpjwkDmta45Etgt3F3Gf5WLZvLbV
To mint tokens:
spl-token mint E75GMXAfJ91XuRboSpjwkDmta45Etgt3F3Gf5WLZvLbV 5 E5XmcEJhhGUri8itThLGk8QfPzY1acFid8JmVyo5DWUo
To transfer tokens:
spl-token transfer <MINT> 1 <ATA>
# or
spl-token transfer <MINT> 1 <RECIPIENT_WALLET>
Let’s create a project that uses the official anchor-spl
crate for minting:
anchor init usdt_spl
cargo add anchor-spl
Add SPL imports in your contract:
use anchor_spl::token::{self, MintTo, Token, TokenAccount, Mint};
Define the mint context:
#[derive(Accounts)]
pub struct MintToCtx<'info> {
#[account(mut)]
pub mint: Account<'info, Mint>,
#[account(mut)]
pub to: Account<'info, TokenAccount>,
#[account(mut)]
pub authority: Signer<'info>,
pub token_program: Program<'info, Token>,
}
Implement conversion to CPI context:
impl<'info> From<&MintToCtx<'info>> for CpiContext<'_, '_, '_, 'info, MintTo<'info>>
{
fn from(accts: &MintToCtx<'info>) -> Self {
let cpi_accounts = MintTo {
mint: accts.mint.to_account_info(),
to: accts.to.to_account_info(),
authority: accts.authority.to_account_info(),
};
CpiContext::new(accts.token_program.to_account_info(), cpi_accounts)
}
}
Add program logic:
pub fn mint_to(ctx: Context<MintToCtx>, amount: u64) -> Result<()> {
token::mint_to((&*ctx.accounts).into(), amount)
}
Update Cargo.toml
features:
[features]
idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"]
[dependencies]
anchor-spl = { version = "0.31.1", features = ["token", "idl-build"] }
Compile:
anchor build
Install required dependencies:
npm i @coral-xyz/anchor@^0.31 @solana/spl-token chai
Test file tests/usdt_spl.ts
:
import anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import {
createMint,
createAssociatedTokenAccount,
getAccount,
TOKEN_PROGRAM_ID,
} from "@solana/spl-token";
import { assert } from "chai";
const { AnchorProvider, BN } = anchor;
describe("usdt_spl / mint_to", () => {
const provider = AnchorProvider.env();
anchor.setProvider(provider);
const program = anchor.workspace.UsdtSpl as Program;
let mintPubkey: anchor.web3.PublicKey;
let ata: anchor.web3.PublicKey;
it("creates mint, mints 1 USDT into ATA", async () => {
mintPubkey = await createMint(
provider.connection,
provider.wallet.payer,
provider.wallet.publicKey,
null,
6
);
ata = await createAssociatedTokenAccount(
provider.connection,
provider.wallet.payer,
mintPubkey,
provider.wallet.publicKey
);
await program.methods
.mintTo(new BN(1_000_000))
.accounts({
mint: mintPubkey,
to: ata,
authority: provider.wallet.publicKey,
tokenProgram: TOKEN_PROGRAM_ID,
})
.rpc();
const accInfo = await getAccount(provider.connection, ata);
assert.equal(accInfo.amount.toString(), "1000000");
});
});
Run the tests:
anchor test
Deploy using anchor:
anchor deploy --provider.cluster devnet
If network issues occur, try:
anchor deploy --provider.cluster "<your-rpc-url>"
# Or
solana program deploy \
target/deploy/usdt_spl.so \
--program-id target/deploy/usdt_spl-keypair.json \
--url "<your-rpc-url>"
File: app/app.js
:
const anchor = require("@coral-xyz/anchor");
const {
createMint,
createAssociatedTokenAccount,
getAccount,
TOKEN_PROGRAM_ID,
} = require("@solana/spl-token");
const fs = require("fs");
const os = require("os");
const path = require("path");
const { Keypair, Connection } = anchor.web3;
const RPC_URL = process.env.RPC_URL || "https://api.devnet.solana.com";
const connection = new Connection(RPC_URL, { commitment: "confirmed" });
const secret = Uint8Array.from(
JSON.parse(fs.readFileSync(path.join(os.homedir(), ".config/solana/id.json")))
);
const wallet = new anchor.Wallet(Keypair.fromSecretKey(secret));
const provider = new anchor.AnchorProvider(connection, wallet, {
preflightCommitment: 'confirmed',
});
anchor.setProvider(provider);
const idl = JSON.parse(fs.readFileSync(path.resolve("target/idl/usdt_spl.json")));
const prog = new anchor.Program(idl, provider);
(async () => {
const mint = await createMint(connection, wallet.payer, wallet.publicKey, null, 6);
const ata = await createAssociatedTokenAccount(connection, wallet.payer, mint, wallet.publicKey);
const sig = await prog.methods
.mintTo(new anchor.BN(1_000_000))
.accounts({ mint, to: ata, authority: wallet.publicKey, tokenProgram: TOKEN_PROGRAM_ID })
.rpc();
console.log("tx:", sig);
console.log(`explorer: https://explorer.solana.com/tx/${sig}?cluster=devnet`);
const bal = await getAccount(connection, ata);
console.log("balance:", bal.amount.toString());
})();
Expected output:
tx: 3MgHxsfnJp68mrrABvCh9iwNm6MSXp1SEvk7vDYHoW7KhTEHfVNyMWsbfbEAXTC9gLzcmWu5xbkzia8hgZrcZ18i
explorer: https://explorer.solana.com/tx/3MgHxsfnJp68mrrABvCh9iwNm6MSXp1SEvk7vDYHoW7KhTEHfVNyMWsbfbEAXTC9gLzcmWu5xbkzia8hgZrcZ18i?cluster=devnet
balance: 1000000